/*
* 1.无向图 双连通分量
    (1) 边的双连通分量  e-dcc  极大的不包含桥的连通块
    (2) 点的双连通分量  v-dcc  极大的不包含割点的连通块

* 2. 什么是桥, 如何找桥？ 
    桥: 删掉该边, 连通分量数量改变
    割点: 删掉该点以及附属边, 连通分量数量改变
    x和y之间是桥 <-> dfn[x] < low[y] y无论如何往上走不到x 
                    +y能到的最高的点low[y] = dfn[y]

    每个割点至少属于两个连通分量
    树里的每条边都是桥

* 3.如何找所有边的双连通分量？
        将所有桥删掉
        dfn[x] == low[x] 
        <-> x无论如何走不到x的上面
        <-> 从x开始的子树可以用一个栈存

* 本题: 
    新建一条道路 使得每两个草场之间都有一个分离的路径
    给定一个无向连通图，问最少加几条边，可以将其变为一个边双连通分量

    结论: 一个边的双连通分量 <-> 任何两个点之间至少存在两个不相交路径
    充分性: 对于每两个点都有互相分离的路径的话，则必然为强连通分量
    反证 假设有桥(非双连通) x,y必然经过中间的桥 则只x->y的路径必在桥上相交

    必要性: 图是一个边双连通分量 <-> 不包含桥
            则一定对任意两点x,y  -> x,y之间至少存在两条互相分离(不相交)的路径

    反证: 假设存在两条相交路径
        那么x->y中间必然有桥

        可以发现对左右两个叶子节点连通后，根节点连向左右叶子节点的边就可以删去了
        同理 再把第2个和第4个叶子节点连通后，根节点连向第2个和第4个叶子节点的边也可以删去
        第3个叶子节点随便连
        给叶子节点按对称性加上边后就没有桥 <-> 变成边的双连通分量
        这里cnt= 5 加了ans=(cnt+1)/2=3条
        ans >= 下界[cnt/2]取整 == [(cnt+1)/2]取整
            其中 cnt为缩完点后度数==1的点的个数
*/

#pragma GCC optimize("O1,O2,O3,Ofast")
#pragma GCC optimize("no-stack-protector,unroll-loops,fast-math,inline")
#pragma GCC target("avx,avx2,fma")
#pragma GCC target("sse,sse2,sse3,sse4,sse4.1,sse4.2,ssse3")

#include<iostream>
#include<cstring>
#include<algorithm>
#include<unordered_set>
#include<stack>
// #define ONLINE_GUDGE
using namespace std;
using LL = long long;
const int N = 5010, M = 20010, INF = 0x3f3f3f3f;

int n, m;
int h[N], e[M], ne[M], idx;
int dfn[N], low[N], timestamp; // dfs序 最小访问时间 时间戳
// int stk[N], top; // 栈 栈顶指针
stack<int> stk; // 内置DS优化
// bool in_stk[N]; // 判断结点是否在栈中
int id[N], scc_cnt, sze[N]; // id数组存储每个点所在的连通分量编号 sze数组统计每个连通分量的点的数量
bool is_bridge[N]; // 记录边是否是桥边
int du[N]; // 记录节点i的度

void AddEdge(int a, int b) // , int c)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++; // w[idx] = c,  
}

void Tarjan(int u, int fa){ // 从第fa条边到u
    dfn[u] = low[u] = ++timestamp;
    // stk[ ++ top ] = u, in_stk[u] = true;
     
    stk.push(u); 
    // in_stk[u] = true;

    for(int i = h[u]; ~i; i = ne[i]){
        int v = e[i];
        if(!dfn[v]){

            // cerr << "u->v: " << " u: " << u << " v: " << v << endl;

            Tarjan(v, i);
            low[u] = min(low[u], low[v]); // 用u的邻点j更新u所能到达的结点最小时间戳
            if(dfn[u] < low[v]) // 表示v节点永远都走不到u节点(u,v这两个点只能从u走到v)，所以边u-v(w[i])是一条桥边
                is_bridge[i] = is_bridge[i^1] = 1; // 因为一对正向边和反向边的编号都是成对出现，所以直接^1即可
        }
        else if(i != (fa^1))
            low[u] = min(low[u], dfn[v]);
    }

    if(dfn[u] == low[u]){ // u是最靠上的节点
        ++ scc_cnt; // 连通分量 + 1
        int y;
        do{
            // y = stk[ top -- ];
            y = stk.top(); stk.pop();
            // in_stk[y] = false;
            id[y] = scc_cnt;
            // sze[scc_cnt] ++;
        }while(y != u);
    }
}

int main()
{

    #ifdef ONLINE_JUDGE

    #else
    freopen("./in.txt","r",stdin);
    #endif
    ios::sync_with_stdio(false);   
	cin.tie(0);
    
    cin >> n >> m;
    memset(h, -1, sizeof h);

    for (int i = 1; i <= m; i ++ )
    {
        int a, b; cin >> a >> b;
        AddEdge(a, b), AddEdge(b, a);
    }

    for(int i = 1; i <= n; ++i){
        if(!dfn[i]) Tarjan(i, -1);  // 引入from防止反向遍历回遍历过的边
    }

    // 遍历所有边，如果边i是桥边,在其所连的出边的点j所在双连通分量的度+1
    for(int i = 0; i < idx; ++i){   // 包含正向边和反向边，所以度为1的节点一定是叶子节点
        if(is_bridge[i]){
            du[id[e[i]]]++;
        }
    }

    int cnt = 0;

    //枚举所有双连通分量，需要加的边数就是:(度为1的节点个数 + 1 ) / 2
    for(int i = 1; i <= scc_cnt; ++i){
        if(du[i] == 1) cnt++;
    }
    cout << (cnt + 1) / 2 << endl;

    return 0;
}
